﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading;
using System.Transactions;
using System.Xml;

using LostLib.Collections.Generic;
using LostLib.Programs.Properties;


namespace LostLib.Programs
{	
	/// <summary>
	/// This class exposes some methods to access programs database, install and remove AssemblyInfos e.t.c.
	/// </summary>
	public static class AssemblyManager
	{
		#region Database
		/// <summary>
		/// Creates local database for LostLib.Programs.
		/// </summary>
		public static void CreateDatabase()
		{
			if (appDB.DatabaseExists())
				throw new InvalidOperationException("LostLib.Programs is installed.\nYou must remove previous installation first.");

			CliAssemblyManager.WriteDebug("creating new database...");
			if (appDB != null)
			{
				appDB.Dispose();
				appDB = new ApplicationsDatabaseDataContext(Settings.Default.ProgramsDB);
			}

			appDB.CreateDatabase();

			CliAssemblyManager.WriteDebug("OK\n");

			CliAssemblyManager.WriteDebug("creating {0}...", Settings.Default.AssemblyCache);
			Directory.CreateDirectory(Settings.Default.AssemblyCache);
			CliAssemblyManager.WriteDebug("OK\n");
		}

		/// <summary>
		/// Determines whether local database is installed.
		/// </summary>
		public static bool Installed
		{
			get
			{
				return appDB.DatabaseExists();
			}
		}

		static ApplicationsDatabaseDataContext appDB = new ApplicationsDatabaseDataContext(Settings.Default.ProgramsDB);

		/// <summary>
		/// Gets local LostLib.Programs database.
		/// </summary>
		public static ApplicationsDatabaseDataContext ApplicationDatabase
		{ 
			get 
			{
				if (!Installed) throw new InvalidOperationException("LostLib.Programs is not installed");
				return appDB;
			}
		}
		#endregion Database

		#region Source updating
		static void UpdateAssemblyVersions(AssemblySource source, AssemblyInfo Assembly, XmlNode AssemblyNode)
		{
			foreach (XmlNode versionNode in AssemblyNode.SelectNodes("version"))
			{
				var version = AssemblyVersion.Parse(versionNode);
				version.AssemblyName = Assembly.AssemblyName;
				version.SourceID = source.Name;

				var current = Assembly.AssemblyVersions.Where(ver => ver.VersionString == version.VersionString).FirstOrDefault();
				version = current ?? version;

				version.SourceID = source.Name;

				if (current != null) continue;

				appDB.AssemblyVersions.InsertOnSubmit(version);

				foreach (XmlNode dependencyNode in versionNode.SelectNodes("dependency"))
				{
					var dependency = VersionDependency.Parse(dependencyNode);
					dependency.VersionString = version.VersionString;

					appDB.VersionDependancies.InsertOnSubmit(dependency);
				}
			}
		}
		internal static void UpdateSourceInternal(string sourceName)
		{
			//TODO: rewrite via LINQ to XML
			CliAssemblyManager.WriteDebug("updating source {0}...", sourceName);
			var source = appDB.AssemblySources.Where(src => src.Name == sourceName).FirstOrDefault();

			if (source == null)
				throw new KeyNotFoundException(sourceName);

			var xml = new XmlDocument();
			xml.Load(source.Uri);
			foreach (var version in source.AssemblyVersions)
				version.SourceID = null;

			foreach (XmlNode AssemblyNode in xml.SelectNodes("/AssemblyDB/assembly"))
			{
				var Assembly = AssemblyInfo.Parse(AssemblyNode);

				var current = appDB.AssemblyInfos.Where(pkg => pkg.AssemblyName == Assembly.AssemblyName).FirstOrDefault();
				Assembly = current ?? Assembly;

				if (current == null)
					appDB.AssemblyInfos.InsertOnSubmit(Assembly);

				UpdateAssemblyVersions(source, Assembly, AssemblyNode);
			}

			appDB.AssemblyVersions.DeleteAllOnSubmit(appDB.AssemblyVersions.Where(ver => ver.SourceID == null));
			RemoveEmptyAssemblyInfos();

			source.Version = DateTime.Now;
			// appDB.SubmitChanges();
			CliAssemblyManager.WriteDebug("OK\n");
		}
		/// <summary>
		/// Downloads latest Assembly list for specified source.
		/// </summary>
		/// <param name="sourceName">Name of source to update.</param>
		/// <remarks>This function has special handling for built-in source.</remarks>
		public static void UpdateSource(string sourceName)
		{
			UpdateSourceInternal(sourceName);
			GlobalAssemblyCache.UpdateCache();
		}
		#endregion Source updating

		#region Remove functions
		//TODO: clean garbage
		static void BuildRemoveList(IEnumerable<AssemblyVersion> removeList, List<AssemblyVersion> removeOrder, HashSet<AssemblyVersion> removed)
		{
			if (removeOrder== null) throw new ArgumentNullException("removed");

			foreach (var version in removeList)
			{
				if (removed.Contains(version)) continue;
				if (!version.Installed) continue;

				removeOrder.Add(version);
				removed.Add(version);

				var toRemove =
					appDB.AssemblyVersions.Where(versionToRemove =>
						(versionToRemove.VersionDependancies.Count(dependency => 
							(dependency.DependencyType == "newer")
							&& (dependency.AssemblyName == version.AssemblyName)) > 0)).ToArray();


				Func<AssemblyVersion, bool> virtualNeedsRemove =
					ver => {
						foreach (var solve in ver.ResolvedDependancies)
							if (!removed.Contains(solve)) return false;
						return true;
					};

				toRemove = toRemove.Where(ver =>
					!ver.VirtualAssembly ||
					virtualNeedsRemove(ver)).ToArray();

				BuildRemoveList(toRemove, removeOrder, removed);
			}
		}
		public static AssemblyVersion[] BuildRemoveList(IEnumerable<AssemblyVersionFilter> AssemblyFilters)
		{
			var removeItems = new List<AssemblyVersion>();
			foreach (var filter in AssemblyFilters)
			{
				var removeVersions = filter.Filter(
					appDB.AssemblyVersions);


				BuildRemoveList(removeVersions, removeItems, new HashSet<AssemblyVersion>());
			}

			removeItems.Reverse();
			return removeItems.ToArray();
		}
		static void remove(AssemblyVersion version)
		{
			if (!version.Installed) return;

			CliAssemblyManager.WriteDebug("removing {0}...", version);

			if (!version.VirtualAssembly)
			{
				GlobalAssemblyCache.Remove(version.FullName.FullName);
			}

			// appDB.SubmitChanges();
		}
		static void remove(IEnumerable<AssemblyVersion> removeList)
		{
			foreach (var version in removeList)
				Download(version.VersionString);

			foreach (var version in removeList)
				remove(version);
		}
		/// <summary>
		/// Removes selected and dependant AssemblyInfos.
		/// Approves removing after building remove order.
		/// </summary>
		/// <param name="versionFilters">List of filters describing initial AssemblyInfos to remove.</param>
		public static void Remove(IEnumerable<AssemblyVersionFilter> versionFilters)
		{
			GlobalAssemblyCache.UpdateCache();
			CliAssemblyManager.WriteDebug("building remove list...");
			var list = BuildRemoveList(versionFilters);
			CliAssemblyManager.WriteDebug("OK\n");

			if (!RemoveApprover.Approve(typeof(AssemblyManager), list)) return;

			remove(list);
			GlobalAssemblyCache.UpdateCache();
		}
		public static void Remove(AssemblyVersion assemblyVersion)
		{
			Remove(new AssemblyVersionFilter[] { new AssemblyVersionFilter { AssemblyName = assemblyVersion.AssemblyName, Version = assemblyVersion.Version } });
		}
		#endregion Remove functions

		/// <summary>
		/// Downloads specified Assembly version to cache.
		/// </summary>
		/// <param name="versionStringValue">Version to download.</param>
		/// <remarks>Doesn't edit database.</remarks>
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1720:IdentifiersShouldNotContainTypeNames", MessageId = "string")]
		public static void Download(string versionStringValue)
		{
			var version = appDB.AssemblyVersions
				.Where(ver => ver.VersionString == versionStringValue)
				.FirstOrDefault();

			if (version == null) throw new KeyNotFoundException(versionStringValue);

			if ((version.LocalFileName != null) && File.Exists(version.LocalFileName)) return;

			CliAssemblyManager.WriteDebug("downloading {0}...", version);
			using (var client = new WebClient())
				client.DownloadFile(new Uri(version.Uri), version.LocalFileName);
			CliAssemblyManager.WriteDebug("OK\n");
		}

		/// <summary>
		/// Rebuilds suitable list.
		/// </summary>
		public static void RebuildSuitableVersionList()
		{
			CliAssemblyManager.WriteDebug("rebuilding suitable version list...");

			foreach (var version in appDB.AssemblyVersions)
				version.Suitable = version.Installed;

			appDB.SubmitChanges();

			int added = 0;
			do
			{
				added = 0;
				foreach (var version in appDB.AssemblyVersions.Where(ver => !ver.Suitable))
				{
					Debug.Assert(!version.Suitable);
					if (version.VirtualAssembly)
					{
						version.Suitable = version.VersionDependancies.Where(dependency =>
							dependency.Resolvers.Where(resolver => resolver.Suitable).Count() > 0).Count() > 0;
						if (version.Suitable) added++;
					} else
					{
						bool notSuitable = false;
						foreach (var dependency in version.VersionDependancies)
							if (dependency.Resolvers.Where(resolver => resolver.Suitable).Count() == 0)
							{
								notSuitable = true;
								break;
							}
						version.Suitable = !notSuitable;
						if (version.Suitable) added++;
					}
				}
				appDB.SubmitChanges();
			} while (added > 0);

			CliAssemblyManager.WriteDebug("OK\n");
		}

		#region Installation functions
		static void install(AssemblyVersion version)
		{
			CliAssemblyManager.WriteDebug("installing {0}...", version);
			if (!version.VirtualAssembly)
			{
				GlobalAssemblyCache.Add(version.LocalFileName);
			}

			// appDB.SubmitChanges();
		}
		public static AssemblyVersion[] BuildInstallList(IEnumerable<AssemblyVersionFilter> AssemblyFilters)
		{
			// TODO: consider using ToLookup
			var dependencyTree = new VersionDependencyTree();
			var roots = new List<AssemblyVersion>();
			foreach (var filter in AssemblyFilters)
				roots.Add(filter.BuildDependencyTree(appDB, filter, dependencyTree));

			return dependencyTree.TopologicalSort();
		}
		public static AssemblyVersion[] BuildInstallList(AssemblyVersion assemblyVersion)
		{
			return BuildInstallList(new AssemblyVersionFilter[]{
				new AssemblyVersionFilter{
					AssemblyName = assemblyVersion.AssemblyName,
					Version = assemblyVersion.Version,
				}});
		}
		static void install(IEnumerable<AssemblyVersion> installOrder)
		{
			foreach (var installItem in installOrder)
				if (!installItem.Cached && !installItem.VirtualAssembly)
					Download(installItem.VersionString);

			foreach (var installItem in installOrder)
				install(installItem);
		}

		/// <summary>
		/// Approver for installations.
		/// </summary>
		public static event EventHandler<ApproveEventArgs<IEnumerable<AssemblyVersion>>> InstallApprover;
		/// <summary>
		/// Approver for removing.
		/// </summary>
		public static event EventHandler<ApproveEventArgs<IEnumerable<AssemblyVersion>>> RemoveApprover;
		/// <summary>
		/// Installs selected AssemblyInfos and their dependancies.
		/// Approves installation after building install list.
		/// </summary>
		/// <param name="AssemblyFilters">List of filters describing initial AssemblyInfos to install.</param>
		public static void Install(IEnumerable<AssemblyVersionFilter> AssemblyFilters)
		{
			GlobalAssemblyCache.UpdateCache();
			CliAssemblyManager.WriteDebug("building install list...");
			var installOrder = BuildInstallList(AssemblyFilters);
			CliAssemblyManager.WriteDebug("OK\n");

			if (!InstallApprover.Approve(typeof(AssemblyManager), installOrder))
				return;

			install(installOrder);
			GlobalAssemblyCache.UpdateCache();
		}

		public static void Install(AssemblyVersion assemblyVersion)
		{
			Install(new AssemblyVersionFilter[]{
				new AssemblyVersionFilter{
					AssemblyName = assemblyVersion.AssemblyName,
					Version = assemblyVersion.Version,
				}});
		}
		#endregion Installation functions

		#region Configuration
		/// <summary>
		/// Removes source by its name.
		/// </summary>
		/// <param name="sourceName">Name of source to delete.</param>
		public static void RemoveSource(string sourceName)
		{
			if (sourceName == "built-in") throw new InvalidOperationException("can't remove built-in source");
			CliAssemblyManager.WriteDebug("removing source {0}...", sourceName);
			//using (var transaction = new TransactionScope())
			//{
				var source = appDB.AssemblySources
					.Where(src => src.Name == sourceName)
					.FirstOrDefault();

				if (source == null) throw new KeyNotFoundException(sourceName);

				appDB.AssemblyVersions.DeleteAllOnSubmit(source.AssemblyVersions);
				appDB.AssemblySources.DeleteOnSubmit(source);

				appDB.SubmitChanges();

				RemoveEmptyAssemblyInfos();

			//    transaction.Complete();
			//}
			CliAssemblyManager.WriteDebug("OK\n");
		}

		/// <summary>
		/// Adds source to database.
		/// </summary>
		/// <param name="sourceName">Name for source.</param>
		/// <param name="uri">Source uri.</param>
		public static void AddSource(string sourceName, Uri uri)
		{
			var source = new AssemblySource() {
				Name = sourceName,
				Uri = uri.ToString(),
				Version = AssemblySource.Never,
			};

			appDB.AssemblySources.InsertOnSubmit(source);
			appDB.SubmitChanges();
		}
		/// <summary>
		/// Adds source to database.
		/// </summary>
		/// <param name="sourceName">Name for source.</param>
		/// <param name="uri">Source uri.</param>
		public static void AddSource(string sourceName, string uri)
		{
			var source = new AssemblySource() {
				Name = sourceName,
				Uri = uri,
				Version = AssemblySource.Never,
			};

			appDB.AssemblySources.InsertOnSubmit(source);
			appDB.SubmitChanges();
		}
		#endregion Configuration

		#region Utils
		static void RemoveEmptyAssemblyInfos()
		{
			appDB.AssemblyInfos.DeleteAllOnSubmit(
				appDB.AssemblyInfos
				.Where(Assembly => Assembly.AssemblyVersions.Count == 0));

			appDB.SubmitChanges();
		}
		#endregion Utils
	}
}
